﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.IO;
using ExtensionLoader;
using ExtensionLoader.Config;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using CableBeachMessages;
using OpenSimDbLinq;

namespace InventoryServer.Extensions
{
    public class OpenSimAssets : IExtension<InventoryServer>, IAssetProvider
    {
        const string EXTENSION_NAME = "OpenSimAssets"; // Used in metrics reporting

        InventoryServer server;
        OpenSimDatabase assetDatabase;

        public OpenSimAssets()
        {
        }

        public bool Start(InventoryServer server)
        {
            this.server = server;

            #region Config Loading

            try
            {
                IConfig extensionConfig = server.ConfigFile.Configs["OpenSimAssetDatabase"];
                assetDatabase = OpenSimUtils.LoadDatabaseFromConfig(extensionConfig);
                
                try
                {
                    // DEBUG:
                    //assetDatabase.Log = Console.Out;

                    // Run a query to select the creation date of the first asset in the database to confirm
                    // that database access is working
                    var creationDateQuery = from asset in assetDatabase.Assets select asset.CreateTime;
                    int result = creationDateQuery.FirstOrDefault();
                    if (result != 0)
                    {
                        DateTime creationDate = OpenMetaverse.Utils.UnixTimeToDateTime(result);
                        Logger.Info("[OpenSimAssets] Connected to asset database that was created on " + creationDate);
                    }
                    else
                    {
                        Logger.Warn("[OpenSimAssets] Could not find any assets in the database. Either a connection problem or the database is empty");
                    }
                }
                catch (Exception ex)
                {
                    Logger.Error("[OpenSimAssets] Failed to establish a database connection: " + ex.Message);
                    return false;
                }
            }
            catch (Exception)
            {
                Logger.Error("[OpenSimAssets] Failed to load [OpenSimAssetDatabase] section from " + InventoryServer.CONFIG_FILE);
                return false;
            }

            #endregion Config Loading

            return true;
        }

        public void Stop()
        {
        }

        public BackendResponse TryFetchMetadata(UUID assetID, out MetadataBlock metadata)
        {
            metadata = null;
            BackendResponse response;

            try
            {
                lock (assetDatabase)
                {
                    var query = from asset in assetDatabase.Assets
                                where asset.ID == assetID.ToString()
                                select new { asset.ID, asset.AssetType, asset.CreateTime, asset.Description, asset.Name, asset.Temporary };

                    var dbMetadata = query.SingleOrDefault();

                    if (dbMetadata != null)
                    {
                        metadata = CreateMetadata(dbMetadata.AssetType, dbMetadata.CreateTime, dbMetadata.Description, dbMetadata.ID,
                            dbMetadata.Name, dbMetadata.Temporary);

                        response = BackendResponse.Success;
                    }
                    else
                    {
                        response = BackendResponse.NotFound;
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenSimAssets] Database fetch failed: " + ex.Message);
                response = BackendResponse.Failure;
            }

            server.MetricsProvider.LogAssetMetadataFetch(EXTENSION_NAME, response, assetID, DateTime.Now);
            return response;
        }

        public BackendResponse TryFetchData(UUID assetID, out byte[] assetData)
        {
            assetData = null;
            string assetIDString = assetID.ToString();
            BackendResponse response;

            try
            {
                lock (assetDatabase)
                {
                    var query = from asset in assetDatabase.Assets where asset.ID == assetIDString select asset.Data;

                    assetData = query.SingleOrDefault();

                    if (assetData != null)
                        response = BackendResponse.Success;
                    else
                        response = BackendResponse.NotFound;
                }
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenSimAssets] Database fetch failed: " + ex.Message);
                response = BackendResponse.Failure;
            }

            return response;
        }

        public BackendResponse TryFetchDataMetadata(UUID assetID, out MetadataBlock metadata, out byte[] assetData)
        {
            metadata = null;
            assetData = null;
            string assetIDString = assetID.ToString();
            BackendResponse response;

            try
            {
                lock (assetDatabase)
                {
                    var query = from asset in assetDatabase.Assets
                                where asset.ID == assetIDString
                                select new { asset.ID, asset.AssetType, asset.CreateTime, asset.Data, asset.Description, asset.Name, asset.Temporary };

                    var dbAsset = query.SingleOrDefault();

                    if (dbAsset.ID == assetIDString)
                    {
                        metadata = CreateMetadata(dbAsset.AssetType, dbAsset.CreateTime, dbAsset.Description, dbAsset.ID, dbAsset.Name,
                            dbAsset.Temporary);
                        assetData = dbAsset.Data;

                        response = BackendResponse.Success;
                    }
                    else
                    {
                        response = BackendResponse.NotFound;
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenSimAssets] Database fetch failed: " + ex.Message);
                response = BackendResponse.Failure;
            }

            server.MetricsProvider.LogAssetMetadataFetch(EXTENSION_NAME, response, assetID, DateTime.Now);
            server.MetricsProvider.LogAssetDataFetch(EXTENSION_NAME, response, assetID, (assetData != null) ? assetData.Length : 0, DateTime.Now);
            return response;
        }

        public BackendResponse TryCreateAsset(MetadataBlock metadata, byte[] assetData, out UUID assetID)
        {
            BackendResponse response;
            metadata.ID = (metadata.ID != UUID.Zero) ? metadata.ID : UUID.Random();

            Assets asset = new Assets();
            asset.ID = metadata.ID.ToString();
            UpdateDBAsset(ref asset, metadata, assetData);

            // Attempt to insert the asset first. If that fails, try to do an update
            try
            {
                lock (assetDatabase)
                {
                    List<Assets> insertList = new List<Assets>(1);
                    insertList.Add(asset);
                    assetDatabase.Assets.BulkInsert(insertList);
                    response = BackendResponse.Success;
                }
            }
            catch (Exception)
            {
                // TODO: Parse the exception to figure out if it was a primary key error or database connection error
                try
                {
                    lock (assetDatabase)
                    {
                        Assets existingAsset = (from a in assetDatabase.Assets where a.ID == metadata.ID.ToString() select a).First();
                        UpdateDBAsset(ref existingAsset, metadata, assetData);
                        assetDatabase.SubmitChanges();
                        response = BackendResponse.Success;
                    }
                }
                catch (Exception ex)
                {
                    Logger.Error("[OpenSimAssets] Database update failed: " + ex.Message);
                    response = BackendResponse.Failure;
                }
            }

            assetID = metadata.ID;
            server.MetricsProvider.LogAssetCreate(EXTENSION_NAME, response, assetID, assetData.Length, DateTime.Now);
            return response;
        }

        public BackendResponse TryCreateAsset(MetadataBlock metadata, byte[] assetData)
        {
            if (metadata.ID == UUID.Zero)
                throw new ArgumentException("metadata.ID cannot be empty");

            return TryCreateAsset(metadata, assetData, out metadata.ID);
        }

        public int ForEach(Action<MetadataBlock> action, int start, int count)
        {
            int rowCount = 0;

            try
            {
                lock (assetDatabase)
                {
                    var query = (from asset in assetDatabase.Assets
                                 select new { asset.ID, asset.AssetType, asset.CreateTime, asset.Description, asset.Name, asset.Temporary })
                        .Skip(start).Take(count);

                    foreach (var dbMetadata in query)
                    {
                        MetadataBlock metadata = CreateMetadata(dbMetadata.AssetType, dbMetadata.CreateTime,
                            dbMetadata.Description, dbMetadata.ID, dbMetadata.Name, dbMetadata.Temporary);

                        action(metadata);
                        ++rowCount;
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenSimAssets] Database fetch failed: " + ex.Message);
            }

            return rowCount;
        }

        MetadataBlock CreateMetadata(sbyte assetType, int createTime, string description, string uuidString, string name, sbyte temporary)
        {
            MetadataBlock metadata;
            string contentType = CableBeachUtils.SLAssetTypeToContentType(assetType);

            switch (contentType)
            {
                case "image/x-j2c":
                    MetadataJPEG2000 j2kMetadata = new MetadataJPEG2000();
                    // FIXME: Do JPEG2000 decodes and cache the results somewhere
                    j2kMetadata.Components = 0;
                    j2kMetadata.LayerEnds = new int[0];
                    metadata = j2kMetadata;
                    break;
                default:
                    metadata = new MetadataDefault();
                    break;
            }

            metadata = new MetadataDefault();
            metadata.ContentType = contentType;
            metadata.CreationDate = OpenMetaverse.Utils.UnixTimeToDateTime(createTime);
            metadata.Description = description;
            metadata.ID = UUID.Parse(uuidString);
            metadata.Methods = new Dictionary<string, Uri>();
            metadata.Methods["data"] = new Uri(server.HttpUri, "/" + uuidString + "/data");
            metadata.Name = name;
            metadata.SHA256 = OpenMetaverse.Utils.EmptyBytes;
            metadata.Temporary = (temporary != 0);

            return metadata;
        }

        void UpdateDBAsset(ref Assets asset, MetadataBlock metadata, byte[] assetData)
        {
            asset.AccessTime = (int)OpenMetaverse.Utils.DateTimeToUnixTime(DateTime.Now);
            asset.AssetType = (sbyte)CableBeachUtils.ContentTypeToSLAssetType(metadata.ContentType);
            asset.CreateTime = asset.AccessTime;
            asset.Data = assetData;
            asset.Description = metadata.Description;
            asset.Local = 0;
            asset.Name = metadata.Name;
            asset.Temporary = (sbyte)(metadata.Temporary ? 1 : 0);
        }
    }
}
